/*
 * Copyright 2006 - 2007 The MITRE Corporation. All rights reserved.
 * Written by Ayal Spitz
 *
 * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1
 * (the "License"); you may not use this file except in compliance
 * with the License and the following two conditions:
 * 
 * The US Government will not be charged any license fee and/or royalties 
 * related to this software.
 * 
 * Neither name of The MITRE Corporation; nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * You may obtain a copy of the License at
 *
 *     http://www.gnu.org/copyleft/lesser.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */

package org.mitre.streamingRSS;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Set;

import net.iharder.Base64;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.mitre.rome.SyndEntryOutput;
import org.mitre.rome.SyndEntryOutput.MetaEntry;
import org.mitre.utils.HashSetTable;


import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedOutput;

/**
 * The Publisher class is designed to simplify the process of participating in
 * an instantRSS session as an information publisher. This class follows in the
 * footsteps of Ruby on Rails and puts convention over configuration, ie there
 * is an attempt to make smart defaults over long config files.
 * 
 * @author Ayal Spitz
 *
 */
public class Publisher extends org.mitre.streamingRSS.xmpp.Publisher{
// ### Class Fields ###########################################################
	private SyndEntryOutput						syndEntryOutput = null;
	private HashSetTable<String, MetaEntry>		metaEntryTable = null;

	private SyndFeedOutput						syndFeedOutput = null;
	private XMLOutputter						xmlOutputter = null;
	private Format								compactFormat = null;

	private String		userName = null, password = null;

// ### Class Constructor ######################################################
	/**
	 * This constructor allows the developer to setup a new instantRSS
	 * publisher with the given host, username and password. Port defaults to
	 * 5222
	 * 
	 * @param host          The host of the Jabber server to connect to
	 * @param username		The username of the publisher
	 * @param password		The password of the publisher
	 * 
	 * @see org.mitre.streamingRSS.xmpp.Publisher#Constructor(String,int,String,String)
	 */
	public Publisher(String host, String username, String password){
		super(host);

		this.userName = username;
		this.password = password;

		init();
	}
	
	/**
	 * This constructor allows the developer to setup a new instantRSS
	 * publisher with the given host, port, username and password
	 * 
	 * @param host          The host of the Jabber server to connect to
	 * @param port          The port of the Jabber server to connect to
	 * @param username		The username of the publisher
	 * @param password		The password of the publisher
	 * 
	 * @see org.mitre.streamingRSS.xmpp.Publisher#Constructor(String,int,String,String)
	 */
	public Publisher(String host, int port, String username, String password){
		super(host, port);
		
		this.userName = username;
		this.password = password;

		init();
	}

	/**
	 * This constructor allows the developer to setup a new instantRSS
	 * publisher with the given username and password
	 * 
	 * @param username		The username of the publisher
	 * @param password		The password of the publisher
	 * 
	 * @see org.mitre.streamingRSS.xmpp.Publisher#Constructor(String,String)
	 */
	public Publisher(String username,String password){
		super();
		
		this.userName = username;
		this.password = password;

		init();
	}
	
	/**
	 * This constructor defaults to using the 'iRSS_Publisher' as the username
	 * and 'InstantRSS' as the password
	 *
	 */
	public Publisher(){ this("iRSS_Publisher","InstantRSS"); }
	
// ### Class Methods ##########################################################
	public void connectAndLogin() throws XMPPException{ 
		connectAndLogin(this.userName,this.password, true);
	}
	
	/**
	 * This method is ment to be called by the constructor to init global
	 * variables metaEntryTable, syndEntryOutput, syndFeedOutput, compactForman,
	 * xmlOutputter
	 */
	private void init(){
		metaEntryTable = new HashSetTable<String, MetaEntry>();
		syndEntryOutput = new SyndEntryOutput();

		// Init syndFeedOutput so we can convert SyndFeeds to JDOM documents
		syndFeedOutput = new SyndFeedOutput();
		
		// Init the compactFormat so that we can output JDOM document in a
		// compact format
		compactFormat = Format.getCompactFormat();
		compactFormat.setOmitEncoding(true);	
		compactFormat.setOmitDeclaration(true);
		
		// Init the xmlOutputter to the compactFormat so that we can output
		// JDOM documents in a compact format
		xmlOutputter = new XMLOutputter(compactFormat);		
	}
	
	/**
	 * This method publishs a given SyndFeed. The method starts by looking up
	 * the feed to see if a chat room already exists for this feed. If a feed
	 * does not exist then a new chat room is constructed with the title of the
	 * feed. The method will then scan the feed and publish only those entries
	 * which have not been published before.
	 * 
	 * @param feed		The feed to be published
	 * 
	 * @throws XMPPException
	 * @throws FeedException
	 * 
	 * @see org.jivesoftware.smackx.muc.MultiUserChat#destroy(String,String)
	 */
	public void publish(SyndFeed feed) throws XMPPException, FeedException{
		String				feedTitle = null;
		String				feedDesc = null;
		MultiUserChat		muc = null;
		Set<MetaEntry>		metaEntrySet = null;
		
		feedTitle = feed.getTitle();
		feedDesc = encodeDescription(feed);
		
		// Look up the feed to get a MUC object
		if (!topicExists(feedTitle)){ createNewMUC(feedTitle, feedDesc); }
		
		// Collect up all the entries from this feed we haven't posted
		metaEntrySet = getNewMetaEntries(feed);
		// Publish new entries to the chat room
		publish(feedTitle,metaEntrySet);
	}

	/**
	 * This method is used to close a chat room associated with a feed. This
	 * room utilized the MultiUserChat.destroy method to close the room.
	 * 
	 * @param feed			A feed that was previously published
	 * 
	 * @throws XMPPException
	 */
	public void close(SyndFeed feed) throws XMPPException{
		closeTopic(feed.getTitle());
	}
	
// ============================================================================
	/**
	 * This method gets new entries from a feed. This method extracts all the
	 * entries from a feed into a set then it removes all the entries
	 * previously extracted. The remainder are all the entries that have not
	 * been seen previously. The new entries are added to the previously stored
	 * set and returned to the caller
	 * 
	 * @param feed			The feed to scan for new entries
	 * 
	 * @throws FeedException
	 */
	protected Set<MetaEntry> getNewMetaEntries(SyndFeed feed) throws FeedException{
		Set<MetaEntry>		metaEntrySet = null;
		Set<MetaEntry>		oldMetaEntrySet = null;
		String				feedTitle = null;
		
		feedTitle = feed.getTitle();
		metaEntrySet = syndEntryOutput.outputMetaEntry(feed);
		oldMetaEntrySet = metaEntryTable.get(feedTitle);
		if (oldMetaEntrySet != null){ metaEntrySet.removeAll(oldMetaEntrySet); }
		metaEntryTable.putAll(feedTitle,metaEntrySet);

		return metaEntrySet;
	}
	
	/**
	 * This method actually publishes the new entries to the chat room.
	 * 
	 * @param muc			The chat room to publish the entries to
	 * @param metaEntries	A set of entries to publish to the chat room
	 * 
	 * @throws XMPPException
	 */
	protected void publish(String mucName, Set<MetaEntry> metaEntries)
		throws XMPPException{
		ArrayList<String>		msgs = null;
		
		msgs = new ArrayList<String>();
		for (MetaEntry metaEntry:metaEntries){ msgs.add(encodeEntry(metaEntry)); }
		publish(mucName,msgs);
	}
	
	protected String encodeDescription(SyndFeed feed){	
		Document			doc = null;
		Element				element = null;
		String				desc = "";
		
		try{			
			doc = syndFeedOutput.outputJDom(feed);
			element = doc.getRootElement().getChild("channel");
			element.removeChildren("item");

			desc = xmlOutputter.outputString(doc);
			// Compress and convert the data with GZIP and Base64
			desc = Base64.encodeBytes(desc.getBytes("UTF-8"), Base64.GZIP | Base64.DONT_BREAK_LINES);
		}catch(Exception e){ e.printStackTrace(); }
		
		return desc;
	}

	/**
	 * This method is a simple wrapper method that calls the entires getString
	 * method.
	 *  
	 * @param entry			The entry to be encoded for publication
	 * 
	 * @return				A string representing the encoded entry
	 * 
	 * @see spitz.ayal.rome.SyndEntryOutput.MetaEntry#getString();
	 */
	protected String encodeEntry(MetaEntry entry){ 
		String			msg = null;

		msg = entry.getString();
		
		try {
			msg = Base64.encodeBytes(msg.getBytes("UTF-8"), Base64.GZIP | Base64.DONT_BREAK_LINES);
		} catch (UnsupportedEncodingException e){ e.printStackTrace(); }
		
		return msg;
	}	
}
